/*
Copyright (C) 2022-2025 ApeCloud Co., Ltd

This file is part of KubeBlocks project

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package component

import (
	"fmt"
	"reflect"

	"golang.org/x/crypto/bcrypt"
	"golang.org/x/exp/maps"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/sets"
	"sigs.k8s.io/controller-runtime/pkg/client"

	appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1"
	appsutil "github.com/apecloud/kubeblocks/controllers/apps/util"
	"github.com/apecloud/kubeblocks/pkg/common"
	"github.com/apecloud/kubeblocks/pkg/constant"
	"github.com/apecloud/kubeblocks/pkg/controller/builder"
	"github.com/apecloud/kubeblocks/pkg/controller/component"
	"github.com/apecloud/kubeblocks/pkg/controller/factory"
	"github.com/apecloud/kubeblocks/pkg/controller/graph"
	"github.com/apecloud/kubeblocks/pkg/controller/model"
	ctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil"
)

const (
	systemAccountLabel          = "apps.kubeblocks.io/system-account"
	systemAccountHashAnnotation = "apps.kubeblocks.io/system-account-hash"
	maximumPasswordLength       = 64 // bcrypt has a maximum length limit of 72, the maximum length generated by KB is 32.
)

var (
	errPasswordTooLong = fmt.Errorf("password length exceeds %d bytes", maximumPasswordLength)
)

// componentAccountTransformer handles component system accounts.
type componentAccountTransformer struct{}

var _ graph.Transformer = &componentAccountTransformer{}

func (t *componentAccountTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error {
	transCtx, _ := ctx.(*componentTransformContext)
	if isCompDeleting(transCtx.ComponentOrig) {
		return nil
	}
	if common.IsCompactMode(transCtx.ComponentOrig.Annotations) {
		transCtx.V(1).Info("Component is in compact mode, no need to create account related objects", "component", client.ObjectKeyFromObject(transCtx.ComponentOrig))
		return nil
	}

	synthesizedComp := transCtx.SynthesizeComponent
	graphCli, _ := transCtx.Client.(model.GraphClient)

	// exist account objects
	secrets, err := listSystemAccountObjects(ctx, synthesizedComp)
	if err != nil {
		return err
	}
	runningNameSet := sets.New(maps.Keys(secrets)...)

	// proto accounts
	accounts, err := synthesizeSystemAccounts(transCtx.CompDef.Spec.SystemAccounts,
		transCtx.Component.Spec.SystemAccounts, false)
	if err != nil {
		return err
	}
	protoNameSet := sets.New(maps.Keys(accounts)...)

	createSet, deleteSet, updateSet := setDiff(runningNameSet, protoNameSet)

	for _, name := range sets.List(createSet) {
		if err := t.createAccount(transCtx, dag, graphCli, accounts[name]); err != nil {
			return err
		}
	}

	for _, name := range sets.List(deleteSet) {
		t.deleteAccount(transCtx, dag, graphCli, secrets[name])
	}

	for _, name := range sets.List(updateSet) {
		if err := t.updateAccount(transCtx, dag, graphCli, accounts[name], secrets[name]); err != nil {
			return err
		}
	}

	return nil
}

func (t *componentAccountTransformer) createAccount(transCtx *componentTransformContext,
	dag *graph.DAG, graphCli model.GraphClient, account synthesizedSystemAccount) error {
	secret, err := t.buildAccountSecret(transCtx, transCtx.SynthesizeComponent, account)
	if err != nil {
		return err
	}
	if err = t.buildAccountHash(account, nil, secret); err != nil {
		return err
	}
	graphCli.Create(dag, secret, appsutil.InUniversalContext4G())
	return nil
}

func (t *componentAccountTransformer) deleteAccount(transCtx *componentTransformContext,
	dag *graph.DAG, graphCli model.GraphClient, secret *corev1.Secret) {
	graphCli.Delete(dag, secret, appsutil.InUniversalContext4G())
}

func (t *componentAccountTransformer) updateAccount(transCtx *componentTransformContext,
	dag *graph.DAG, graphCli model.GraphClient, account synthesizedSystemAccount, running *corev1.Secret) error {
	secret, err := t.buildAccountSecret(transCtx, transCtx.SynthesizeComponent, account)
	if err != nil {
		return err
	}
	if err = t.buildAccountHash(account, running, secret); err != nil {
		return err
	}

	runningCopy := running.DeepCopy()
	if account.SecretRef != nil {
		// sync password from the external secret
		runningCopy.Data[constant.AccountPasswdForSecret] = secret.Data[constant.AccountPasswdForSecret]
	}
	ctrlutil.MergeMetadataMapInplace(secret.Labels, &runningCopy.Labels)
	ctrlutil.MergeMetadataMapInplace(secret.Annotations, &runningCopy.Annotations)
	if !reflect.DeepEqual(running, runningCopy) {
		graphCli.Update(dag, running, runningCopy, appsutil.InUniversalContext4G())
	}
	return nil
}

func (t *componentAccountTransformer) buildAccountHash(account synthesizedSystemAccount, running, secret *corev1.Secret) error {
	if account.SecretRef == nil {
		return nil
	}
	if running != nil {
		hashedPassword := running.Annotations[systemAccountHashAnnotation]
		if verifySystemAccountPassword(secret, []byte(hashedPassword)) {
			if secret.Annotations == nil {
				secret.Annotations = map[string]string{}
			}
			secret.Annotations[systemAccountHashAnnotation] = hashedPassword
			return nil // have the same password
		}
	}
	return signatureSystemAccountPassword(secret)
}

func (t *componentAccountTransformer) buildAccountSecret(ctx *componentTransformContext,
	synthesizeComp *component.SynthesizedComponent, account synthesizedSystemAccount) (*corev1.Secret, error) {
	var password []byte
	var err error
	switch {
	case account.SecretRef != nil:
		if password, err = t.getPasswordFromSecret(ctx, account); err != nil {
			return nil, err
		}
	default:
		password, err = t.buildPassword(ctx, account)
		if err != nil {
			return nil, err
		}
	}
	if len(password) > maximumPasswordLength {
		return nil, errPasswordTooLong
	}
	return t.buildAccountSecretWithPassword(ctx, synthesizeComp, account, password)
}

func (t *componentAccountTransformer) getPasswordFromSecret(ctx graph.TransformContext, account synthesizedSystemAccount) ([]byte, error) {
	secretKey := types.NamespacedName{
		Namespace: account.SecretRef.Namespace,
		Name:      account.SecretRef.Name,
	}
	secret := &corev1.Secret{}
	if err := ctx.GetClient().Get(ctx.GetContext(), secretKey, secret); err != nil {
		return nil, err
	}

	passwordKey := constant.AccountPasswdForSecret
	if len(account.SecretRef.Password) > 0 {
		passwordKey = account.SecretRef.Password
	}
	if len(secret.Data) == 0 || len(secret.Data[passwordKey]) == 0 {
		return nil, fmt.Errorf("referenced account secret has no required credential field: %s", passwordKey)
	}
	return secret.Data[passwordKey], nil
}

func (t *componentAccountTransformer) buildPassword(ctx *componentTransformContext, account synthesizedSystemAccount) ([]byte, error) {
	// get restore password if exists during recovery.
	password, err := appsutil.GetRestoreSystemAccountPassword(ctx.Context, ctx.Client,
		ctx.SynthesizeComponent.Annotations, ctx.SynthesizeComponent.Name, account.Name)
	if err != nil {
		return nil, fmt.Errorf("failed to restore password for system account %s of component %s from annotation", account.Name, ctx.SynthesizeComponent.Name)
	}
	if account.InitAccount && len(password) == 0 {
		// initAccount can also restore from factory.GetRestoreSystemAccountPassword(ctx.SynthesizeComponent, account).
		// This is compatibility processing.
		password = []byte(factory.GetRestorePassword(ctx.SynthesizeComponent))
	}
	if len(password) == 0 {
		password, err := common.GeneratePasswordByConfig(account.PasswordGenerationPolicy)
		return []byte(password), err
	}
	return password, nil
}

func (t *componentAccountTransformer) buildAccountSecretWithPassword(ctx *componentTransformContext,
	synthesizeComp *component.SynthesizedComponent, account synthesizedSystemAccount, password []byte) (*corev1.Secret, error) {
	secretName := constant.GenerateAccountSecretName(synthesizeComp.ClusterName, synthesizeComp.Name, account.Name)
	secret := builder.NewSecretBuilder(synthesizeComp.Namespace, secretName).
		// Priority: static < dynamic < built-in
		AddLabelsInMap(synthesizeComp.StaticLabels).
		AddLabelsInMap(synthesizeComp.DynamicLabels).
		AddLabelsInMap(constant.GetCompLabels(synthesizeComp.ClusterName, synthesizeComp.Name)).
		AddLabels(systemAccountLabel, account.Name).
		AddAnnotationsInMap(synthesizeComp.StaticAnnotations).
		AddAnnotationsInMap(synthesizeComp.DynamicAnnotations).
		PutData(constant.AccountNameForSecret, []byte(account.Name)).
		PutData(constant.AccountPasswdForSecret, password).
		// SetImmutable(true).
		GetObject()
	if err := setCompOwnershipNFinalizer(ctx.Component, secret); err != nil {
		return nil, err
	}
	return secret, nil
}

func listSystemAccountObjects(ctx graph.TransformContext,
	synthesizedComp *component.SynthesizedComponent) (map[string]*corev1.Secret, error) {
	opts := []client.ListOption{
		client.InNamespace(synthesizedComp.Namespace),
		client.MatchingLabels(constant.GetCompLabels(synthesizedComp.ClusterName, synthesizedComp.Name)),
	}
	secretList := &corev1.SecretList{}
	if err := ctx.GetClient().List(ctx.GetContext(), secretList, opts...); err != nil {
		return nil, err
	}

	m := make(map[string]*corev1.Secret)
	for i, secret := range secretList.Items {
		if accountName, ok := secret.Labels[systemAccountLabel]; ok {
			m[accountName] = &secretList.Items[i]
		}
	}
	return m, nil
}

func signatureSystemAccountPassword(secret *corev1.Secret) error {
	password := secret.Data[constant.AccountPasswdForSecret]
	hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	if secret.Annotations == nil {
		secret.Annotations = map[string]string{}
	}
	secret.Annotations[systemAccountHashAnnotation] = string(hashedPassword)
	return nil
}

func verifySystemAccountPassword(secret *corev1.Secret, hashedPassword []byte) bool {
	password := secret.Data[constant.AccountPasswdForSecret]
	err := bcrypt.CompareHashAndPassword(hashedPassword, password)
	return err == nil
}

type synthesizedSystemAccount struct {
	appsv1.SystemAccount
	Disabled  *bool
	SecretRef *appsv1.ProvisionSecretRef
}

func synthesizeSystemAccounts(compDefAccounts []appsv1.SystemAccount,
	compAccounts []appsv1.ComponentSystemAccount, keepDisabled bool) (map[string]synthesizedSystemAccount, error) {
	accounts := make(map[string]synthesizedSystemAccount)
	for _, account := range compDefAccounts {
		accounts[account.Name] = synthesizedSystemAccount{
			SystemAccount: account,
		}
	}

	merge := func(account synthesizedSystemAccount, compAccount appsv1.ComponentSystemAccount) synthesizedSystemAccount {
		if compAccount.PasswordConfig != nil {
			account.PasswordGenerationPolicy = *compAccount.PasswordConfig
		}
		account.Disabled = compAccount.Disabled
		account.SecretRef = compAccount.SecretRef
		return account
	}

	for i := range compAccounts {
		account, ok := accounts[compAccounts[i].Name]
		if !ok {
			return nil, fmt.Errorf("system account %s not defined in component definition", compAccounts[i].Name)
		}
		accounts[account.Name] = merge(account, compAccounts[i])
	}

	if !keepDisabled {
		for _, name := range maps.Keys(accounts) {
			account := accounts[name]
			if account.Disabled != nil && *account.Disabled {
				if account.InitAccount {
					return nil, fmt.Errorf("cannot disable init system account: %s", name)
				}
				delete(accounts, name)
			}
		}
	}
	return accounts, nil
}
